package service

import (
	"context"
	"fmt"
	"github.com/golang/protobuf/ptypes/empty"
	"github.com/pkg/errors"
	"gorm.io/gorm"
	"inventory/api/inventory"
	"inventory/internal/conf"
	"inventory/internal/constant"
	"inventory/internal/dao"
	"inventory/internal/model"
	"inventory/internal/pkg/generator"
	"inventory/pkg/ecode"
	"inventory/pkg/env"
	"inventory/pkg/request_key"
	"inventory/pkg/util"
	"inventory/pkg/wechat"
	"sort"
	"strconv"
	"strings"
	"time"
)

func (s *Service) verifyWarehouse(ctx context.Context, warehouseIds []uint32, sgId uint32) (
	warehouseMap model.WarehouseMap, err error) {
	var warehouses []*model.Warehouse
	warehouseMap = make(model.WarehouseMap)
	if warehouses, err = s.dao.FindWarehousesByIDs(ctx, warehouseIds, sgId); err != nil {
		return
	}
	if err = warehouseMap.InitFromWarehouseArray(warehouses, sgId, true); err != nil {
		return
	}
	return
}

func (s *Service) loadDetailBoms(ctx context.Context, req *model.FreezeProcessRequest) (err error) {
	if len(req.DirectStorageSet) == 0 {
		return
	}
	var bomArray []*model.BomConfig
	if bomArray, err = s.dao.FindBomByStorageKeys(ctx, req.DirectStorageSet.GetKeys()); err != nil {
		return
	}
	for _, b := range bomArray {
		materialKey := "m-" + strconv.Itoa(int(b.MaterialID))
		if _, ok := req.DirectBomMap[b.StorageKey]; !ok {
			req.DirectBomMap[b.StorageKey] = make(map[string]*model.BomConfig)
		}
		if _, ok := req.ReverseBomMap[materialKey]; !ok {
			req.ReverseBomMap[materialKey] = make(util.StringBoolMap)
		}
		req.DirectBomMap[b.StorageKey][materialKey] = b
		req.ReverseBomMap[materialKey][req.DetailNameMap[b.StorageKey]] = true
	}
	for warehouseId, detailMap := range req.DetailMap {
		for storageKey, detail := range detailMap {
			if _, ok := req.DirectStorageSet[storageKey]; ok {
				if _, initOK := req.DirectWarehouseQueryMap[warehouseId]; !initOK {
					req.DirectWarehouseQueryMap[warehouseId] = make(map[string]float64)
				}
				if bomMap, hasBom := req.DirectBomMap[storageKey]; hasBom {
					for materialKey, bomConfig := range bomMap {
						if _, hasMaterialKey := req.DirectWarehouseQueryMap[warehouseId][materialKey]; !hasMaterialKey {
							req.DirectWarehouseQueryMap[warehouseId][materialKey] = 0
						}
						req.DirectWarehouseQueryMap[warehouseId][materialKey] += detail.Quantity * bomConfig.Quantity
					}
				} else {
					req.DirectWarehouseQueryMap[warehouseId][storageKey] = detail.Quantity
				}
			}
		}
	}
	return
}

func (s *Service) verifyFreezeDetail(d *inventory.CreateFreezeOrderDetail,
	req *model.FreezeProcessRequest, quantity float64, refDetailId uint64) (err error) {
	warehouseId := d.Id
	if _, ok := req.DetailMap[warehouseId]; !ok {
		req.DetailMap[warehouseId] = make(map[string]*model.FreezeProcessDetail)
	}
	if _, ok := req.DetailMap[warehouseId][d.StorageKey]; !ok {
		req.DetailMap[warehouseId][d.StorageKey] = &model.FreezeProcessDetail{
			StorageKey:        d.StorageKey,
			FreezeOrderNumber: d.FreezeOrderNumber,
			Quantity:          quantity,
		}
		req.DetailNameMap[d.StorageKey] = d.StorageName
		switch d.StorageType {
		case inventory.StorageType_PROD:
			req.DetailMap[warehouseId][d.StorageKey].StorageType = uint8(d.StorageType)
		default:
			err = ecode.BadRequest("冻结单明细类型不合法")
			return
		}
		if d.FreezeOrderNumber != "" {
			if req.Source != inventory.SourceEnum_XT_ORDER {
				err = ecode.BadRequest("仅星团订单支持使用活动下单")
				return
			}
			if _, fOK := req.RefFreezeKeys[d.FreezeOrderNumber]; !fOK {
				req.RefFreezeKeys[d.FreezeOrderNumber] = make([]string, 0)
			}
			// 这里为活动key
			req.RefFreezeKeys[d.FreezeOrderNumber] = append(
				req.RefFreezeKeys[d.FreezeOrderNumber], fmt.Sprintf("%d-%s", warehouseId, d.StorageKey))
		} else {
			// 冻结单模式的无需进行库存推送
			req.AffectWarehouseSet[warehouseId] = false
			if refDetailId == 0 {
				// 需要查询bom
				req.DirectStorageSet[d.StorageKey] = true
			}
		}
	} else {
		// 冻结单明细内不允许出现重复的商品关键字
		err = constant.ErrDuplicateStorageKeys
		return
	}
	return
}

func (s *Service) verifyFreezeQuantity(d *inventory.CreateFreezeOrderDetail) (quantity float64, err error) {
	if d.Quantity <= 0 {
		err = ecode.BadRequest("明细数量必须为正数")
		return
	}
	quantity = float64(d.Quantity)
	return
}

func (s *Service) verifyFreezeDetails(ctx context.Context, details []*inventory.CreateFreezeOrderDetail,
	req *model.FreezeProcessRequest) (err error) {
	req.DetailCount = len(details)
	if req.DetailCount == 0 {
		err = ecode.BadRequest("明细数量不允许为0")
		return
	}
	for _, d := range details {
		var quantity float64
		if quantity, err = s.verifyFreezeQuantity(d); err != nil {
			return
		}
		if err = s.verifyFreezeDetail(d, req, quantity, 0); err != nil {
			return
		}
	}
	// 校验仓库
	if req.WarehouseMap, err = s.verifyWarehouse(ctx, req.GetWarehouseKeys(), req.SgId); err != nil {
		return
	}
	// 加载bom
	if err = s.loadDetailBoms(ctx, req); err != nil {
		return
	}
	return
}

func (s *Service) freezeDirectStorage(ctx context.Context, freezeReq *model.FreezeProcessRequest,
	now time.Time) (err error) {
	for warehouseId, queryMap := range freezeReq.DirectWarehouseQueryMap {
		var (
			storageArray []*model.Storage
			affected     bool
			queryKeys    []string
		)
		for queryKey := range queryMap {
			queryKeys = append(queryKeys, queryKey)
		}
		if storageArray, err = s.dao.FindWarehouseStoragesByStorageKeys(ctx, warehouseId, queryKeys); err != nil {
			return
		}
		storageMap := make(model.StorageMap)
		storageMap.InitStorageMap(storageArray)
		if freezeReq.WarehouseMap[warehouseId].StorageMode == constant.WarehouseStorageModeStrong {
			if len(storageMap) != len(queryKeys) {
				// 强管控模式下，所有仓储对象必须存在
				for _, storageKey := range queryKeys {
					if !util.InStringArray(storageKey, storageMap.GetKeys()) {
						freezeReq.AppendLackDetail(storageKey)
					}
				}
				break
			}
			// 先排序再锁表，以防止互斥锁
			sort.Strings(queryKeys)
			for _, storageKey := range queryKeys {
				storage := storageMap[storageKey]
				targetQuantity := queryMap[storageKey]
				availableQuantity := storage.Quantity - storage.FreezeQuantity
				leftQuantity := availableQuantity - targetQuantity
				if leftQuantity < 0 {
					freezeReq.AppendLackDetail(storageKey)
					break
				}
				if targetQuantity > 0 && (leftQuantity < 10 || availableQuantity/targetQuantity <= 2) {
					// 单次购买超出总量的一半 / 剩余不足10份时，均补偿锁表, 规避超卖场景
					if err = s.dao.Lock(ctx, model.Storage{WarehouseID: warehouseId, StorageKey: storageKey},
						&storage); err != nil {
						return
					}
					leftQuantity = storage.Quantity - storage.FreezeQuantity - targetQuantity
				}
				// 增量更新
				if affected, err = s.dao.FreezeStorage(
					ctx, warehouseId, storageKey, targetQuantity, now); err != nil {
					return
				}
				if affected {
					freezeReq.AffectWarehouseSet[warehouseId] = true
				}
			}
		} else {
			for _, storageKey := range queryKeys {
				if _, ok := storageMap[storageKey]; ok {
					// 增量更新
					targetQuantity := queryMap[storageKey]
					if affected, err = s.dao.FreezeStorage(
						ctx, warehouseId, storageKey, targetQuantity, now); err != nil {
						return
					}
					if affected {
						freezeReq.AffectWarehouseSet[warehouseId] = true
					}
				}
			}
		}
	}
	return
}

func (s *Service) freezeActivityStorage(ctx context.Context, freezeReq *model.FreezeProcessRequest,
	now time.Time) (err error) {
	// 从活动转移冻结库存
	if len(freezeReq.RefFreezeKeys) != 0 {
		var (
			freezeOrders  []*model.FreezeOrder
			freezeDetails []*model.FreezeDetail
		)
		freezeOrderMap := make(util.UInt64StringMap)
		if freezeOrders, err = s.dao.LockFreezeOrdersByOrderNumbers(ctx, freezeReq.GetFreezeOrderNumbers()); err != nil {
			return
		}
		for _, freezeOrder := range freezeOrders {
			freezeOrderMap[freezeOrder.ID] = freezeOrder.OrderNumber
			freezeReq.RefFreezeMap[freezeOrder.OrderNumber] = &model.ActivityOrder{
				FreezeOrder: freezeOrder,
				DetailMap:   make(map[string]*model.ActivityDetail),
			}
		}
		if len(freezeOrders) != len(freezeReq.RefFreezeKeys) {
			// 有效活动数量与实际数量不匹配时，活动所有商品均库存不足
			for freezeOrderNumber, activityKeys := range freezeReq.RefFreezeKeys {
				if _, ok := freezeReq.RefFreezeMap[freezeOrderNumber]; !ok {
					for _, activityKey := range activityKeys {
						freezeReq.AppendLackDetail(strings.SplitN(activityKey, "-", 2)[1])
					}
				}
			}
		}
		if freezeDetails, err = s.dao.FindFreezeValidDetailsByOrderIds(ctx, freezeOrderMap.GetKeys()); err != nil {
			return
		}
		activityDetailMap := make(util.UInt64StringMap)
		for _, freezeDetail := range freezeDetails {
			if freezeDetail.ParentID == 0 {
				activityKey := fmt.Sprintf("%d-%s", freezeDetail.WarehouseID, freezeDetail.StorageKey)
				activityDetailMap[freezeDetail.ID] = activityKey
				freezeReq.RefFreezeMap[freezeOrderMap[freezeDetail.OrderID]].DetailMap[activityKey] = &model.ActivityDetail{
					ID:                freezeDetail.ID,
					StorageKey:        freezeDetail.StorageKey,
					StorageType:       freezeDetail.StorageType,
					AvailableQuantity: freezeDetail.ValidQuantity,
					Subs:              make(map[float64]*model.ActivityDetail),
				}
			} else {
				activityKey := activityDetailMap[freezeDetail.ParentID]
				refQuantity := freezeDetail.ValidQuantity / freezeReq.RefFreezeMap[freezeOrderMap[freezeDetail.OrderID]].DetailMap[activityKey].AvailableQuantity
				freezeReq.RefFreezeMap[freezeOrderMap[freezeDetail.OrderID]].DetailMap[activityKey].Subs[refQuantity] = &model.ActivityDetail{
					ID:                freezeDetail.ID,
					StorageKey:        freezeDetail.StorageKey,
					StorageType:       freezeDetail.StorageType,
					AvailableQuantity: freezeDetail.ValidQuantity,
				}
			}
		}
		for freezeOrderNumber, activityKeys := range freezeReq.RefFreezeKeys {
			for _, activityKey := range activityKeys {
				mergedKeys := strings.SplitN(activityKey, "-", 2)
				activityDetail, ok := freezeReq.RefFreezeMap[freezeOrderNumber].DetailMap[activityKey]
				if !ok {
					freezeReq.AppendLackDetail(mergedKeys[1])
					continue
				}
				warehouseId, _ := strconv.Atoi(mergedKeys[0])
				detail := freezeReq.DetailMap[uint32(warehouseId)][mergedKeys[1]]
				if detail.Quantity > activityDetail.AvailableQuantity {
					freezeReq.AppendLackDetail(detail.StorageKey)
					continue
				}
				activityDetail.AvailableQuantity -= detail.Quantity
				if err = s.dao.TransferFreezeDetail(ctx, activityDetail.ID, detail.Quantity, now); err != nil {
					return
				}
				if len(activityDetail.Subs) != 0 {
					for refQuantity, subActivityDetail := range activityDetail.Subs {
						if err = s.dao.TransferFreezeDetail(
							ctx, subActivityDetail.ID, detail.Quantity*refQuantity, now); err != nil {
							return
						}
					}
				}
			}
			// 与星团沟通，确定活动库存即使售罄也无需关闭活动
		}
	}
	return
}

func (s *Service) createFreezeDetails(ctx context.Context, freezeReq *model.FreezeProcessRequest,
	details []*inventory.CreateFreezeOrderDetail, now time.Time, orderId uint64) (err error) {
	for _, d := range details {
		// freezeMap中目前并没有已配置bom的原始商品冻结数量
		warehouseId := d.Id
		mappingDetail := freezeReq.DetailMap[warehouseId][d.StorageKey]
		detail := model.FreezeDetail{
			OrderID:                 orderId,
			StorageType:             mappingDetail.StorageType,
			StorageKey:              mappingDetail.StorageKey,
			WarehouseID:             warehouseId,
			ParentID:                0,
			TargetQuantity:          mappingDetail.Quantity,
			ConsumedQuantity:        0,
			ValidQuantity:           mappingDetail.Quantity,
			OriginFreezeOrderNumber: mappingDetail.FreezeOrderNumber,
			CreateTime:              now,
			UpdateTime:              now,
			ProdName:                d.StorageName,
			SkuName:                 d.SkuName,
		}
		if mappingDetail.FreezeOrderNumber == "" {
			if bomMap, ok := freezeReq.DirectBomMap[d.StorageKey]; ok {
				if err = s.dao.Create(ctx, &detail); err != nil {
					return
				}
				for storageKey, bomConfig := range bomMap {
					childQuantity := mappingDetail.Quantity * bomConfig.Quantity
					child := model.FreezeDetail{
						OrderID:                 orderId,
						StorageType:             uint8(inventory.StorageType_MATERIAL),
						StorageKey:              storageKey,
						WarehouseID:             warehouseId,
						ParentID:                detail.ID,
						TargetQuantity:          childQuantity,
						ConsumedQuantity:        0,
						ValidQuantity:           childQuantity,
						IsAccounting:            true,
						OriginFreezeOrderNumber: "",
						CreateTime:              now,
						UpdateTime:              now,
					}
					if err = s.dao.Create(ctx, &child); err != nil {
						return
					}
				}
			} else {
				detail.IsAccounting = true
				if err = s.dao.Create(ctx, &detail); err != nil {
					return
				}
			}
		} else {
			subs := freezeReq.RefFreezeMap[mappingDetail.FreezeOrderNumber].DetailMap[fmt.Sprintf("%d-%s", warehouseId, d.StorageKey)].Subs
			if len(subs) == 0 {
				detail.IsAccounting = true
			}
			if err = s.dao.Create(ctx, &detail); err != nil {
				return
			}
			for refQuantity, subActivityDetail := range subs {
				childQuantity := mappingDetail.Quantity * refQuantity
				child := model.FreezeDetail{
					OrderID:                 orderId,
					StorageType:             uint8(inventory.StorageType_MATERIAL),
					StorageKey:              subActivityDetail.StorageKey,
					WarehouseID:             warehouseId,
					ParentID:                detail.ID,
					TargetQuantity:          childQuantity,
					ConsumedQuantity:        0,
					ValidQuantity:           childQuantity,
					IsAccounting:            true,
					OriginFreezeOrderNumber: mappingDetail.FreezeOrderNumber,
					CreateTime:              now,
					UpdateTime:              now,
				}
				if err = s.dao.Create(ctx, &child); err != nil {
					return
				}
			}
		}
	}
	return
}

func (s *Service) createFreezeOrder(ctx context.Context, freezeReq *model.FreezeProcessRequest,
	details []*inventory.CreateFreezeOrderDetail, remark []byte, memberId, orderKey string,
	now time.Time) (freezeOrder model.FreezeOrder, err error) {
	// 生成单号
	var orderNumber string
	if memberId != "" {
		orderKey = memberId
	}
	if orderNumber, err = generator.GenerateOrderNumber(
		now, constant.OrderTypeFreezeByCustomer, orderKey); err != nil {
		return
	}
	freezeOrder = model.FreezeOrder{
		OrderNumber: orderNumber,
		RequestNum:  freezeReq.RequestNum,
		Source:      uint8(freezeReq.Source),
		SgID:        freezeReq.SgId,
		DetailCount: uint32(freezeReq.DetailCount),
		Status:      constant.FreezeOrderInit,
		Remark:      remark,
		CreateTime:  now,
		UpdateTime:  now,
		ExpireTime:  freezeReq.ExpireTime,
	}
	// 落地订单
	if err = s.dao.Create(ctx, &freezeOrder); err != nil {
		return
	}
	// 落地明细
	if err = s.createFreezeDetails(ctx, freezeReq, details, now, freezeOrder.ID); err != nil {
		return
	}
	return
}

// CreateFreezeOrder 创建冻结单
func (s *Service) CreateFreezeOrder(ctx context.Context, req *inventory.CreateFreezeOrderRequest) (reply *inventory.CreateOrderReply, err error) {
	var freezeReq *model.FreezeProcessRequest
	if freezeReq, err = model.NewFreezeProcessRequest(req.SgId, req.RequestNum, req.Source, req.ExpireTime); err != nil {
		return
	}
	// 校验明细 & 加载bom
	if err = s.verifyFreezeDetails(ctx, req.Details, freezeReq); err != nil {
		return
	}
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		now := time.Now()
		// 检查配送费是否完成校验
		var (
			calculationRequest *model.ProcessRequest
			freezeOrder        model.FreezeOrder
		)
		if req.Source == inventory.SourceEnum_MALL_ORDER {
			if calculationRequest, err = s.dao.LockCalculatedProcessRequest(ctx, req.RequestNum, uint8(req.Source)); err != nil {
				if errors.Cause(err) == gorm.ErrRecordNotFound {
					err = constant.ErrCalculationNotExist
				}
				return
			}
			if calculationRequest.Status != constant.CalculateRequestInit {
				err = constant.ErrCalculationNotExist
				return
			}
		}
		// 冻结增量库存
		if err = s.freezeDirectStorage(ctx, freezeReq, now); err != nil {
			return
		}
		// 转移活动库存
		if err = s.freezeActivityStorage(ctx, freezeReq, now); err != nil {
			return
		}
		// 库存不足时，构造相关错误
		if err = freezeReq.CheckLackDetails(); err != nil {
			return
		}
		// 落地订单&明细
		if freezeOrder, err = s.createFreezeOrder(
			ctx, freezeReq, req.Details, req.Remark, req.MemberId, req.OrderKey, now); err != nil {
			return
		}
		// 更新配送费校验记录
		if calculationRequest != nil {
			if err = s.dao.UpdateCalculationRequest(ctx, calculationRequest, freezeOrder.ID, now); err != nil {
				return
			}
		}
		reply = &inventory.CreateOrderReply{
			OrderNumber:    freezeOrder.OrderNumber,
			ExtOrderNumber: req.RequestNum,
			Status:         uint32(freezeOrder.Status),
		}
		return
	}); err != nil {
		return
	}
	return
}

// GetFreezeOrder 获取冻结单
func (s *Service) GetFreezeOrder(ctx context.Context, req *inventory.GetFreezeOrderRequest) (reply *inventory.GetFreezeOrderReply, err error) {
	// 获取冻结单
	var freezeOrder *model.FreezeOrder
	if req.OrderNumber != "" {
		if freezeOrder, err = s.dao.GetFreezeOrder(ctx, req.OrderNumber); err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				err = constant.ErrOrderNotExist
			}
			return
		}
	} else {
		if req.RequestNum == "" || req.Source == 0 {
			err = ecode.BadRequest("外部请求号跟来源必传")
			return
		}
		if freezeOrder, err = s.dao.GetFreezeOrderByRequestNumAndSource(ctx, req.RequestNum, int(req.Source)); err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				err = constant.ErrOrderNotExist
			}
			return
		}
	}
	// 获取冻结单详情
	var details []*model.FreezeDetail
	if details, err = s.dao.FindFreezeOrderNonZeroDetails(ctx, freezeOrder.ID); err != nil {
		return
	}
	warehouseIds := make(util.UInt32BoolMap)
	views := make([]*inventory.GetFreezeOderDetail, len(details))
	for _, d := range details {
		warehouseIds[d.WarehouseID] = true
	}
	var warehouses []*model.Warehouse
	warehouseMap := make(model.WarehouseMap)
	if warehouses, err = s.dao.FindWarehousesByIDs(ctx, warehouseIds.GetKeys(), freezeOrder.SgID); err != nil {
		return
	}
	_ = warehouseMap.InitFromWarehouseArray(warehouses, 0, false)
	for i, d := range details {
		if d.ParentID != 0 {
			// 前端无需感知冻结单嵌套结构
			continue
		}
		views[i] = &inventory.GetFreezeOderDetail{
			StorageType:      inventory.StorageType(d.StorageType),
			StorageKey:       d.StorageKey,
			TargetQuantity:   d.TargetQuantity,
			ConsumedQuantity: d.ConsumedQuantity,
			ValidQuantity:    d.ValidQuantity,
			WarehouseId:      d.WarehouseID,
			WarehouseName:    warehouseMap[d.WarehouseID].Name,
			CreateTime:       d.CreateTime.String(),
			ProdName:         d.ProdName,
			SkuName:          d.SkuName,
		}
	}
	reply = &inventory.GetFreezeOrderReply{
		OrderNumber: freezeOrder.OrderNumber,
		Source:      inventory.SourceEnum(freezeOrder.Source),
		RequestNum:  freezeOrder.RequestNum,
		SubmitTime:  freezeOrder.CreateTime.String(),
		Status:      uint32(freezeOrder.Status),
		Details:     views,
		Remark:      freezeOrder.Remark,
	}
	if freezeOrder.ExpireTime.Valid {
		reply.ExpireTime = freezeOrder.ExpireTime.Time.String()
	}
	return
}

func (s *Service) lockFreezeOrderAndDetails(ctx context.Context, orderNumber, requestNum string, sgId uint32, source uint8) (
	freezeOrder *model.FreezeOrder, detailMap map[string]*model.FreezeDetail,
	subMap map[uint64][]*model.FreezeDetail, err error) {
	var details []*model.FreezeDetail
	freezeOrder = &model.FreezeOrder{}
	if orderNumber != "" {
		err = s.dao.Lock(ctx, model.FreezeOrder{
			OrderNumber: orderNumber,
			Source:      source,
			SgID:        sgId,
		}, freezeOrder)
	} else {
		err = s.dao.Lock(ctx, model.FreezeOrder{
			Source:     source,
			RequestNum: requestNum,
			SgID:       sgId,
		}, freezeOrder)
	}
	if err != nil {
		if errors.Cause(err) == gorm.ErrRecordNotFound {
			err = constant.ErrOrderNotExist
			return
		}
		return
	}
	if freezeOrder.Status != constant.FreezeOrderInit {
		err = ecode.BadRequest("只有冻结中的活动冻结单可以更新数量")
		return
	}
	detailMap = make(map[string]*model.FreezeDetail)
	subMap = make(map[uint64][]*model.FreezeDetail)
	if details, err = s.dao.FindFreezeOrderNonZeroDetails(ctx, freezeOrder.ID); err != nil {
		return
	}
	for _, d := range details {
		if d.ParentID != 0 {
			if _, ok := subMap[d.ParentID]; !ok {
				subMap[d.ParentID] = make([]*model.FreezeDetail, 0)
			}
			subMap[d.ParentID] = append(subMap[d.ParentID], d)
		} else {
			detailMap[fmt.Sprintf("%d-%s", d.WarehouseID, d.StorageKey)] = d
		}
	}
	return
}

func (s *Service) verifyUpdatedDetails(ctx context.Context, rawDetails []*inventory.CreateFreezeOrderDetail,
	existDetailMap map[string]*model.FreezeDetail, existSubMap map[uint64][]*model.FreezeDetail,
	freezeReq *model.FreezeProcessRequest, now time.Time) (
	updatedDetails []*model.FreezeDetail, newDetails []*inventory.CreateFreezeOrderDetail, err error) {
	freezeReq.DetailCount = len(rawDetails)
	if freezeReq.DetailCount == 0 {
		err = ecode.BadRequest("明细数量不允许为0")
		return
	}
	for _, d := range rawDetails {
		if d.FreezeOrderNumber != "" {
			err = ecode.BadRequest("冻结单更新明细不支持活动嵌套")
			return
		}
		var quantity float64
		if quantity, err = s.verifyFreezeQuantity(d); err != nil {
			return
		}
		detailKey := fmt.Sprintf("%d-%s", d.Id, d.StorageKey)
		if detail, ok := existDetailMap[detailKey]; ok {
			if detail.TargetQuantity == quantity {
				// 数量不变则跳过
				continue
			}
			diffQuantity := quantity - detail.TargetQuantity
			if err = s.verifyFreezeDetail(d, freezeReq, diffQuantity, detail.ID); err != nil {
				return
			}
			if detail.TargetQuantity > quantity && detail.ValidQuantity > quantity {
				// 追减模式，标注明细更新数量 && 追减目标数量 < 已冻结数量
				err = ecode.BadRequest("所选商品%s目标数量必须大于已冻结数量", d.StorageName)
				return
			}
			// 追加模式需要进行库存检查
			if _, initOK := freezeReq.DirectWarehouseQueryMap[detail.WarehouseID]; !initOK {
				freezeReq.DirectWarehouseQueryMap[detail.WarehouseID] = make(map[string]float64)
			}
			// 更新明细
			if subDetails, hasSubs := existSubMap[detail.ID]; hasSubs {
				for _, subDetail := range subDetails {
					targetQuantity := quantity * subDetail.TargetQuantity / detail.TargetQuantity
					subDiffQuantity := targetQuantity - subDetail.TargetQuantity
					if subDiffQuantity > 0 {
						// 追加模式需要进行库存检查，需要准备映射数据
						if _, isInit := freezeReq.ReverseBomMap[subDetail.StorageKey]; !isInit {
							freezeReq.ReverseBomMap[subDetail.StorageKey] = make(util.StringBoolMap)
						}
						freezeReq.ReverseBomMap[subDetail.StorageKey][d.StorageName] = true
					}
					if _, hasMaterialKey := freezeReq.DirectWarehouseQueryMap[detail.WarehouseID][subDetail.StorageKey]; !hasMaterialKey {
						freezeReq.DirectWarehouseQueryMap[detail.WarehouseID][subDetail.StorageKey] = 0
					}
					freezeReq.DirectWarehouseQueryMap[detail.WarehouseID][subDetail.StorageKey] += subDiffQuantity
					// 更新子明细信息
					subDetail.TargetQuantity = targetQuantity
					subDetail.UpdateTime = now
					updatedDetails = append(updatedDetails, subDetail)
				}
			} else {
				// 无子明细的追加模式
				freezeReq.DirectWarehouseQueryMap[detail.WarehouseID][detail.StorageKey] = diffQuantity
			}
			// 更新主明细信息
			detail.TargetQuantity = quantity
			detail.UpdateTime = now
			updatedDetails = append(updatedDetails, detail)
		} else {
			// 新增明细
			if err = s.verifyFreezeDetail(d, freezeReq, quantity, 0); err != nil {
				return
			}
			newDetails = append(newDetails, d)
		}
	}
	if len(newDetails) != 0 {
		// 校验仓库
		if freezeReq.WarehouseMap, err = s.verifyWarehouse(ctx, freezeReq.GetWarehouseKeys(), freezeReq.SgId); err != nil {
			return
		}
		// 加载bom
		if err = s.loadDetailBoms(ctx, freezeReq); err != nil {
			return
		}
	}
	return
}

// UpdateFreezeOrder 更新冻结单(二期)
func (s *Service) UpdateFreezeOrder(ctx context.Context, req *inventory.UpdateFreezeOrderRequest) (reply *inventory.DefaultResp, err error) {
	if req.Source != inventory.SourceEnum_XT_ACTIVITY {
		err = ecode.BadRequest("仅星团活动支持更新冻结单")
		return
	}
	var freezeReq *model.FreezeProcessRequest
	if freezeReq, err = model.NewFreezeProcessRequest(req.SgId, req.RequestNum, req.Source, req.ExpireTime); err != nil {
		return
	}
	reply = &inventory.DefaultResp{Status: true}
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var (
			freezeOrder *model.FreezeOrder
			detailMap   map[string]*model.FreezeDetail
			subMap      map[uint64][]*model.FreezeDetail
			// 这里为加载明细 & 追减(含已冻结归0)明细
			updatedDetails []*model.FreezeDetail
			// 需要拆分为追加数量明细；新明细
			newDetails []*inventory.CreateFreezeOrderDetail
		)
		now := time.Now()
		// 锁定冻结单 & 加载非空明细
		if freezeOrder, detailMap, subMap, err = s.lockFreezeOrderAndDetails(
			ctx, req.OrderNumber, req.RequestNum, req.SgId, uint8(req.Source)); err != nil {
			return
		}
		// 分解请求明细 => 更新明细 & 新增明细
		if updatedDetails, newDetails, err = s.verifyUpdatedDetails(
			ctx, req.Details, detailMap, subMap, freezeReq, now); err != nil {
			return
		}
		// 冻结增量库存
		if err = s.freezeDirectStorage(ctx, freezeReq, now); err != nil {
			return
		}
		// 库存不足时，构造相关错误
		if err = freezeReq.CheckLackDetails(); err != nil {
			return
		}
		// 订单 & 明细落地
		freezeOrder.DetailCount = uint32(freezeReq.DetailCount)
		freezeOrder.ExpireTime = freezeReq.ExpireTime
		freezeOrder.UpdateTime = now
		if err = s.dao.Save(ctx, freezeOrder); err != nil {
			return
		}
		// 更新明细
		for _, detail := range updatedDetails {
			if err = s.dao.Save(ctx, detail); err != nil {
				return
			}
		}
		// 落地新增明细
		if err = s.createFreezeDetails(ctx, freezeReq, newDetails, now, freezeOrder.ID); err != nil {
			return
		}
		return
	}); err != nil {
		return
	}
	reply = &inventory.DefaultResp{Status: true}
	return
}

// EndFreezeOrder 结束冻结单
func (s *Service) EndFreezeOrder(ctx context.Context, req *inventory.EndFreezeOrderRequest) (reply *inventory.DefaultResp, err error) {
	reply = &inventory.DefaultResp{Status: true}
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		now := time.Now()
		freezeOrder := &model.FreezeOrder{}
		if req.OrderNumber != "" {
			err = s.dao.Lock(ctx, model.FreezeOrder{
				OrderNumber: req.OrderNumber,
			}, freezeOrder)
		} else {
			err = s.dao.Lock(ctx, model.FreezeOrder{
				Source:     uint8(req.Source),
				RequestNum: req.RequestNum,
			}, freezeOrder)
		}
		if err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				// 允许外部因为超时调用解冻，此时允许单号不存在
				err = nil
				return
			}
			return
		}
		switch freezeOrder.Status {
		case constant.FreezeOrderCancelled:
			// 冻结单已经取消，幂等返回不报错
			return
		case constant.FreezeOrderFinished, constant.FreezeOrderProcessing:
			err = ecode.BadRequest("此冻结单存在履约单，不支持直接结束")
			return
		}
		err = s.endFreezeOrder(ctx, freezeOrder, now)
		return
	}); err != nil {
		return
	}
	return
}

// 取消整个冻结单
func (s *Service) endFreezeOrder(ctx context.Context, freezeOrder *model.FreezeOrder, now time.Time) (err error) {
	var details []*model.FreezeDetail
	if details, err = s.dao.FindFreezeOrderNonZeroDetails(ctx, freezeOrder.ID); err != nil {
		return
	}
	storageKeyMap := make(map[uint32]util.StringBoolMap)
	materialIdsMap := make(map[uint32][]uint64)
	warehouseIDMap := make(util.UInt32BoolMap)
	for _, d := range details {
		if d.IsAccounting {
			var affected bool
			if affected, err = s.dao.UnfreezeStorage(
				ctx, d.WarehouseID, d.StorageKey, d.ValidQuantity, now); err != nil {
				return
			}
			if affected {
				if _, ok := storageKeyMap[d.WarehouseID]; !ok {
					storageKeyMap[d.WarehouseID] = make(util.StringBoolMap)
					materialIdsMap[d.WarehouseID] = make([]uint64, 0)
					warehouseIDMap[d.WarehouseID] = true
				}
				storageKeyMap[d.WarehouseID][d.StorageKey] = true
				if inventory.StorageType(d.StorageType) == inventory.StorageType_MATERIAL {
					materialId, _ := strconv.Atoi(d.StorageKey[2:])
					materialIdsMap[d.WarehouseID] = append(materialIdsMap[d.WarehouseID], uint64(materialId))
				}
			}
		}
	}
	freezeOrder.ExpireTime = util.NullTime{}
	freezeOrder.Status = constant.FreezeOrderCancelled
	freezeOrder.UpdateTime = now
	if err = s.dao.Save(ctx, freezeOrder); err != nil {
		return
	}
	return
}

// markFreezeOrderConsumed 标记冻结单已消费
func (s *Service) markFreezeOrderConsumed(ctx context.Context, freezeOrder *model.FreezeOrder, now time.Time) (err error) {
	freezeOrder.Status = constant.FreezeOrderProcessing
	// 清空过期时间，此后只允许手动基于履约单逐个取消
	freezeOrder.ExpireTime = util.NullTime{}
	freezeOrder.UpdateTime = now
	if err = s.dao.Save(ctx, freezeOrder); err != nil {
		return
	}
	if err = s.dao.MarkFreezeDetailConsumed(ctx, freezeOrder.ID, now); err != nil {
		return
	}
	return
}

// updateFreezeOrder 冻结单部分消费&取消
func updateFreezeOrder(ctx context.Context, dao *dao.Dao, freezeOrder *model.FreezeOrder, processOrder *model.ProcessOrder,
	unfreezeMap map[string]float64, now time.Time, isConsume bool) (err error) {
	var (
		details     []*model.FreezeDetail
		materialIds []uint64
	)
	if details, err = dao.FindFreezeOrderDetails(ctx, freezeOrder.ID); err != nil {
		return
	}
	detailMap := make(map[uint32]map[string]*model.FreezeDetail)
	for _, d := range details {
		if _, ok := detailMap[d.WarehouseID]; !ok {
			detailMap[d.WarehouseID] = make(map[string]*model.FreezeDetail)
		}
		detailMap[d.WarehouseID][d.StorageKey] = d
	}
	storageKeyMap := make(util.StringBoolMap)
	for storageKey, unfreezeQuantity := range unfreezeMap {
		d := detailMap[processOrder.WarehouseID][storageKey]
		d.ValidQuantity -= unfreezeQuantity
		if !isConsume {
			d.ConsumedQuantity -= unfreezeQuantity
		}
		if d.IsAccounting {
			if isConsume {
				// 发货模式(发货不改变实际可售数量，无需通知)
				if _, err = dao.DecreaseAndUnfreezeStorage(
					ctx, d.WarehouseID, d.StorageKey, unfreezeQuantity, now); err != nil {
					return
				}
			} else {
				// 取消模式
				var detailAffected bool
				if detailAffected, err = dao.UnfreezeStorage(
					ctx, d.WarehouseID, d.StorageKey, unfreezeQuantity, now); err != nil {
					return
				}
				if detailAffected {
					storageKeyMap[d.StorageKey] = true
					if inventory.StorageType(d.StorageType) == inventory.StorageType_MATERIAL {
						materialId, _ := strconv.Atoi(d.StorageKey[2:])
						materialIds = append(materialIds, uint64(materialId))
					}
				}
			}

		}
		d.UpdateTime = now
		if err = dao.Save(ctx, d); err != nil {
			return
		}
	}
	freezeOrder.UpdateTime = now
	freezeOrder.Status = constant.FreezeOrderFinished
	for _, d := range details {
		if d.ValidQuantity > 0 {
			// 当前冻结单仍存在可出库数量
			freezeOrder.Status = constant.FreezeOrderProcessing
		}
	}
	if err = dao.Save(ctx, freezeOrder); err != nil {
		return
	}
	return
}

// recoverFreezeOrder 基于指定履约单进行冻结单还原扣库操作
func (s *Service) recoverFreezeOrder(ctx context.Context, freezeOrder *model.FreezeOrder,
	processOrder *model.ProcessOrder, recoverMap map[string]float64, now time.Time) (err error) {
	var details []*model.FreezeDetail
	if details, err = s.dao.FindFreezeOrderNonZeroDetails(ctx, freezeOrder.ID); err != nil {
		return
	}
	detailMap := make(map[uint32]map[string]*model.FreezeDetail)
	for _, d := range details {
		if _, ok := detailMap[d.WarehouseID]; !ok {
			detailMap[d.WarehouseID] = make(map[string]*model.FreezeDetail)
		}
		detailMap[d.WarehouseID][d.StorageKey] = d
	}
	for storageKey, recoverQuantity := range recoverMap {
		d := detailMap[processOrder.WarehouseID][storageKey]
		d.ValidQuantity += recoverQuantity
		d.ConsumedQuantity += recoverQuantity
		if d.IsAccounting {
			if _, err = s.dao.DecreaseAndUnfreezeStorage(
				ctx, d.WarehouseID, d.StorageKey, -recoverQuantity, now); err != nil {
				return
			}
		}
		d.UpdateTime = now
		if err = s.dao.Save(ctx, d); err != nil {
			return
		}
	}
	freezeOrder.UpdateTime = now
	freezeOrder.Status = constant.FreezeOrderProcessing
	if err = s.dao.Save(ctx, freezeOrder); err != nil {
		return
	}
	return
}

// 冻结单过期自检
func (s *Service) ExpireCheck(rawCtx context.Context, _ *empty.Empty) (resp *inventory.DefaultResp, err error) {
	reqID := request_key.GetRequestID(rawCtx)
	resp = &inventory.DefaultResp{
		Status: true,
	}
	var freezeOrders []*model.FreezeOrder
	if freezeOrders, err = s.dao.FindExpiredFreezeOrder(rawCtx); err != nil {
		return
	}
	if len(freezeOrders) == 0 {
		return
	}
	go func() {
		ctx := request_key.GenerateContextWithRequestID(reqID)
		// 只负责recover特殊异常
		defer ecode.AsyncFailOver(ctx, "冻结单过期检查", conf.Conf.Env, nil)
		for _, freezeOrder := range freezeOrders {
			if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
				if err = s.dao.Lock(ctx, model.FreezeOrder{
					OrderNumber: freezeOrder.OrderNumber,
				}, freezeOrder); err != nil {
					return
				}
				if freezeOrder.Status != constant.FreezeOrderInit {
					// 状态有变，无需自动过期
					return
				}
				if err = s.endFreezeOrder(ctx, freezeOrder, time.Now()); err != nil {
					return
				}
				return
			}); err != nil {
				if conf.Conf.Env != env.Local {
					wechat.AsyncSendMarkDown(
						ctx, wechat.FormatBugMessage(
							ctx, conf.Conf.Env, conf.Conf.ServiceName, freezeOrder.OrderNumber,
							"冻结单自动过期失败",
							fmt.Sprintf("error: %+v", err)))
				}
				err = nil
			}
		}
	}()
	return
}
